package cheesymock.recorder;

import java.util.BitSet;
import java.util.HashSet;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class RecorderState {
	private short argumentComparerIdentifier;
	private final Queue<ArgumentModifier<? extends Object>> argumentModifiers = new ConcurrentLinkedQueue<ArgumentModifier<? extends Object>>();
	private final Queue<MethodInvocation> correctInvocationsQueue = new ConcurrentLinkedQueue<MethodInvocation>();
	private final Queue<MethodInvocation> expectedInvocationsQueue = new ConcurrentLinkedQueue<MethodInvocation>();

	public RecorderState() {
		resetArgumentComparerIdentifier();
	}

	/**
	 * Check if method call has argument modifiers and add expected method call to expect queue.
	 *
	 * @param methodInvocation
	 */
	public void addExpectedInvocation(MethodInvocation methodInvocation) {
		checkForUnnumberedArguments();
		// For each argument modifier
		while (!argumentModifiers.isEmpty()) {
			final ArgumentModifier<? extends Object> argumentModifier = argumentModifiers.poll();
			final Object[] args = methodInvocation.getArgs();
			if (argumentModifier.getArgumentIndex() == null) {
				findArgumentIndex(argumentModifier, args, methodInvocation.getMethod().getParameterTypes());
			}
			methodInvocation.getArgumentModifiers().add(argumentModifier);
		}
		resetArgumentComparerIdentifier();
		expectedInvocationsQueue.add(methodInvocation);
	}

	private void checkForUnnumberedArguments() {
		HashSet<Object> unnumberedClasses = new HashSet<Object>();
		for (ArgumentModifier<? extends Object> argumentModifier : argumentModifiers) {
			if (argumentModifier.getArgumentIndex() == null) {
				final Object argument = argumentModifier.getArgument();
				if (unnumberedClasses.contains(argument)) {
					final Class<? extends Object> argumentClass = argument.getClass();
					throw new IllegalArgumentException("Ignore cannot reuse same instace of type: " + argumentClass
							+ ", please use new instances for each ignore or number the ignore."
							+ " Example recorder.ignore(new " + argumentClass.getSimpleName()
							+ "()) or recorder.ignore(" + argumentClass.getSimpleName().toLowerCase() + ", 3)");
				} else {
					unnumberedClasses.add(argument);
				}
			}
		}
	}

	public short getArgumentComparerIdentifier() {
		return argumentComparerIdentifier++;
	}

	public Queue<ArgumentModifier<? extends Object>> getArgumentModifiers() {
		return argumentModifiers;
	}

	public Queue<MethodInvocation> getCorrectInvocationsQueue() {
		return correctInvocationsQueue;
	}

	public Queue<MethodInvocation> getExpectedInvocationsQueue() {
		return expectedInvocationsQueue;
	}

	public void reset() {
		resetArgumentComparerIdentifier();
		expectedInvocationsQueue.clear();
		correctInvocationsQueue.clear();
		argumentModifiers.clear();
	}

	private boolean argumentsEqual(Object argumentModifierArg, Object argument, boolean parameterClassPrimitive) {
		if (argumentModifierArg == argument) {
			return true;
		}
		if (parameterClassPrimitive && primitiveEquals(argumentModifierArg, argument)) {
			return true;
		}
		return false;
	}

	/**
	 * Try to determine which argument in the method call that had the argument modifier. Problems could occur with
	 * primitive types or reused object instances.
	 *
	 * @param argumentModifier
	 * @param args
	 * @param parameterClasses
	 */
	private void findArgumentIndex(final ArgumentModifier<? extends Object> argumentModifier, final Object[] args,
			Class<?>[] parameterClasses) {
		Integer foundMatch = null;
		// TODO: This method must return an exception if more than one argument
		// matches the argument modifier
		for (int i = args.length - 1; i >= 0; i--) {
			Object argument = args[i];
			if (argumentsEqual(argumentModifier.getArgument(), argument, parameterClasses[i].isPrimitive())) {
				if (foundMatch != null) {
					throw new IllegalArgumentException("The unnumbered ignore argument matched both parameter nr "
							+ (i + 1) + " and nr " + (foundMatch + 1)
							+ ". Please use a numbered ignore instead. Example recorder.ignore("
							+ argumentModifier.getArgument().getClass().getSimpleName().toLowerCase() + ", " + (i + 1)
							+ ")");
				}
				argumentModifier.setArgumentIndex(i);
				foundMatch = i;
			}
		}
		if (foundMatch == null)
			throw new IllegalStateException("Cannot match argument modifier to any argument");
	}

	private boolean primitiveEquals(Object o1, Object o2) {
		if (o1 instanceof Number && o2 instanceof Number) {
			return ((Number) o1).longValue() == ((Number) o2).longValue();
		}
		if (o1 instanceof Character && o2 instanceof Character) {
			return o1.equals(o2);
		}
		if (o1 instanceof Boolean && o2 instanceof Boolean) {
			return o1.equals(o2);
		}
		return false;
	}

	private void resetArgumentComparerIdentifier() {
		this.argumentComparerIdentifier = Short.MIN_VALUE + 42;
	}
}
